"""EMUI
multi-emulator frontend for EmuFarm
art by 343, code by JPL"""

#
# JPL notes 2009.12.12
#
# This code dates back to 2006, when I was somewhat less experienced
# as a programmer, so I can make no claims about its quality.  It's
# included for historical and reference purposes, and because it still
# pretty much functions if you have the right configuration.
# See the README in this dir for more details.

# --------------------------------------------------------------------

# TODO v1.0 final:
# key/joybinds in config.xml?
# attract mode that boots a random game after X seconds inactivity
# option to power down after Y seconds inactivity

# experimental speedup to arcade list building process:
mame_list_speed = True
log = False

# TODO possible v2.0:
# more robust escape sequence (keyboard support, friendlier process killing?
#  - use that guy's killableprocess.py
# functionality of shutdown.exe built in?
# proper support for unzipped (non-MAME) romsets
# support for multiple archive formats (ZIP, RAR)
# externalize all UI stuff to XML for "theming"
# (soundsets as well as visuals)

import pygame, os, sys, datetime, cPickle, py7zlib
from time import sleep
from random import randint
import xml.sax.saxutils
# when building a standalone EXE for win32, make sure to include
# the _xmlplus package as it contains needed goodies!

# OS-specific ways for creating, monitoring and killing child process :(
if 'win' in sys.platform:
    import win32api, win32process, win32con, win32gui, win32event, win32security
    import win32shutdown
    from socket import gethostname
elif 'linux' in sys.platform:
    import popen2

screenSize = screenWidth, screenHeight = 640, 480
appName = 'EMUI v1.0 beta'
appIcon = 'art/EmuIcon.png'

# global variables

# transient (runtime) stuff
screen = "" # screen that everything will be drawn onto
SystemList = [] # list of systems, to be populated from XML
currentPage = "" # can be "System", "GameFull", "GameFav" or "ROM"
PageList = [] # items accessible from this page
pageSelIndex = 0 # selected place in PageList
pageScreenIndex = 0 # PageList index at top of screen list
ScreenList = [] # items currently on screen
screenListLength = 0 # length of current page
SelectedSystem = None # system that's currently selected
SelectedGame = None
pad = None # joystick to use for input

emuiDir = os.getcwd() # dir EMUI starts in
runningEmu = False # only accept input if False
emuProcess = None # process object for emulator child process
emuPID = None # PID for emuProcess
runningROM = None # name of ROM being run

# duration in seconds EMUI will wait before terminating emu the messy way
emuCloseTimeout = 2.5

# seconds before "attract mode" kicks in
attract_idle_time = 15
idle_time = 0

favsListLength = 20 # length of "recently played" list

# saved places in each page
# these are read from and written to when changing pages
sysSelIndex = 0 # selected item in system list
sysScreenIndex = 0 # item currently at top of system list screen
fullSelIndex = 0 # full game list
fullScreenIndex = 0

# general settings, read from XML
fullscreen = False
playSounds = True

# UI stuff
titleFontFile = 'font/emufarm.ttf'
titleFontSize = 30
subTitleFontSize = 26
listFontFile = 'font/joystix.ttf'
listFontSize = 22
gameListFontSize = 18
gameNumFontSize = 16
descFontSize = 9
tinyFontFile = 'font/verasans.ttf'
tinyFontSize = 16

baseColor = 0, 0, 0
selectBarColor = 255, 255, 102
textColor = 204, 204, 153

sysBGImageFile = 'art/bg_choose_system.png'
sysAllIcon = 'art/Icon_AllSystems.png'
sysExitIcon = 'art/Icon_Windows.png'
sysShutdownIcon = 'art/Icon_ShutDown.png'
sysScreenCharLength = 11 # items in system list
sysScreenCharWidth = 14
sysDescWidth = 27 # char width of description field in system list
sysDescLength = 24
sysListOffsetX = 84 # distance from left edge of screen system list items begin
sysListOffsetY = 128 # distance from top
sysListWidth = 260 # width of system list
sysListHeight = 284
sysDescOffsetX = 355 # system description text offset
sysDescOffsetY = 125
sysDescWidth = 200
sysDescHeight = 250
sysIconOffsetX = 393 # system portrait icon offset
sysIconOffsetY = 385

gameBGImageFile = 'art/bg_choose_game.png'
gameScreenCharLength = 16 # number of vertical items in game/rom list
gameScreenCharWidth = 51
gameListWidth = 614 # width of game list
gameListHeight = 312
gameListOffsetX = 13 # distance from left edge of screen game list items begin
gameListOffsetY = 92 # distance from top
gameNumOffsetXneg = 84 # distance from RIGHT edge of screen "X of Y" item begins
gameNumOffsetY = 403 # distance from top
fullToggleOffsetX = 114 # distance from left edge for "Full List"
fullToggleOffsetY = 43
toggleWidth = 148
toggleHeight = 24
favToggleOffsetX = 374 # distance from left edge for "Favs List"
favToggleOffsetY = fullToggleOffsetY

romBGImageFile = 'art/bg_choose_version.png'
# for now, rom list uses same layout numbers as game list
romGameNameWidth = 616 # width of game name area
romGameNameHeight = 46
romGameNameOffsetX = 12
romGameNameOffsetY = 400

sleepImageFile = 'art/Loading.png'

# text strings
allSysName = 'All Systems'
allSysDescription = 'Select a game from all available systems!'
exitName = 'Exit to Desktop'
exitDescription = 'Exit to the Desktop'
shutdownName = 'Shut Down'
shutdownDescription = 'Shut down the EMUFARM'
noDescText = 'No description has been entered for this system.'
fullText = 'Full List'
favText = 'Recent'

# keyboard bindings
yesKeys = [pygame.K_RETURN, pygame.K_SPACE, pygame.K_q]
noKeys = [pygame.K_BACKSPACE, pygame.K_w]
randomKeys = [pygame.K_KP_MULTIPLY, pygame.K_e]
upKeys = [pygame.K_UP, pygame.K_KP8]
downKeys = [pygame.K_DOWN, pygame.K_KP2]
pgUpKeys = [pygame.K_PAGEUP, pygame.K_KP9, pygame.K_y]
pgDnKeys = [pygame.K_PAGEDOWN, pygame.K_KP3, pygame.K_b]
fullListKeys = [pygame.K_LEFT, pygame.K_KP4, pygame.K_g]
favsListKeys = [pygame.K_RIGHT, pygame.K_KP6, pygame.K_h]

inputDelay = 250 # milliseconds before input starts repeat
inputRepeatRate = 100 # milliseconds per input repeat

# "escape sequence": when all these buttons are pressed together,
# the emulator will exit and you'll return to the EMUI
escSeqButtons = [0, 1, 2, 3]

# joystick bindings
stickNumber = 0 # system device number, eg 0 or 1
yesButtons = [0]
noButtons = [1]
randomButtons = [2]
pgUpButtons = [5]
pgDnButtons = [4]
toFullButtons = [7]
toFavButtons = [6]
selAxis = 1
stickDeadZone = 0.1 # joystick "dead zone" to minimize erroneous inputs

# sounds
yesSoundFile = 'sound/yes.wav'
noSoundFile = 'sound/no.wav'
randomSoundFile = 'sound/random.wav'
selSoundFile = 'sound/selmove.wav'
pageSoundFile = 'sound/pagemove.wav'

# utility functions

def EscSequenceEntered():
    """returns true if the special 'exit emulator' button combo is
    engaged.  this is only possible with joysticks, because only one
    app can have keyboard focus at any one time in both windows and
    linux.  to get around this in windows, use a 'virtual joystick'
    program such as PPJoy to map keyboard inputs to joystick buttons."""
    entered = True
    # make sure events are in the queue!
    pygame.event.pump()
    if pad is not None:
        for b in escSeqButtons:
            if pad.get_button(b) == 0:
                entered = False
        if entered is True:
            return True
    return False

def CoerceSystemName(sysName):
    "returns a System object for the specified system name"
    for system in SystemList:
        if system not in [allSysName, exitName, shutdownName]:
            if system.name == sysName:
                return system

def CoerceGameName(gameName, sysName):
    "returns a game object for the specified game and system names"
    for game in CoerceSystemName(sysName).GameList:
        if game.name == gameName:
            return game

def StringToBool(value):
    """takes a string (presumably from XML) and returns
    a boolean representation - errs on the side of False"""
    if value is not None:
        if value.lower() == 'true' or value == '1':
            return True
    return False

def SwitchWinDir():
    """windows apps like to have everything in their directory,
    so we switch to keep it from complaining and pooping
    config files everywhere"""
    if 'win' in sys.platform:
        os.chdir(os.path.dirname(SelectedGame.system.emuPath))

def StripExtension(filename):
    return filename[0:filename.rfind('.')]

def FindMameName(f, mameList):
    "tries to find a proper name for a MAME rom file from the provided list"
    filename = os.path.basename(f)
    # special case for MAME roms: only use zip files!
    # "mame content manager" renames unwanted roms to .mcm
    if not filename.lower().endswith('.zip'):
        return (None, None)
    filename = StripExtension(filename)
    i = 0
    for line in mameList:
        # 'filename' goes from start of string to first space
        # example line:
        # mspacmnf  "Ms. Pac-Man (with speedup hack)"
        line = line.replace('\n', '') # strip newlines
        line = line.replace('\r', '')
        # 'file name' ends at first whitespace
        if line[0:line.find(' ')] == filename.lower():
            # 'friendly name' is between the quotes
            nom = line[line.find('"')+1:line.rfind('"')]
            # super-verbose logging for mame list generation
            # troubleshooting - set False to True to enable
            if log and False:
                print 'name found for ROM %s: "%s"' % (filename, nom)
            return (nom, i)
        i += 1
    return (None, None)

def WalkDir(dirname, fileList):
    "recursively lists the contents of dirname and stores it in fileList"
    for d in os.listdir(dirname):
        pathname = os.path.join(dirname, d)
        if os.path.isdir(pathname) is True:
            WalkDir(pathname, fileList)
        elif os.path.isfile(pathname) is True:
            fileList.append(pathname)

def LoadFavsList(system):
    """returns a FavsList for the specified system, read
    from data stored in previous sessions"""
    FavsList = []
    favsListName = 'fav/%s.fav' % system.name
    if os.path.exists(favsListName) is True:
        favsListFile = file(favsListName, 'rb')
        FavsList = cPickle.Unpickler(favsListFile).load()
        favsListFile.close()
    return FavsList

def StoreFavsList(system):
    """writes the current state of the specified system's Favorites list to a file
    (usually called after changing the FavsList, by running a game)"""
    favsListName = 'fav/%s.fav' % system.name
    favsListFile = file(favsListName, 'wb')
    cPickle.Pickler(favsListFile, cPickle.HIGHEST_PROTOCOL).dump(system.FavsList)
    favsListFile.close()

def AddToFavsList(game, rom):
    """adds the specified version of a game to the specified
    system's Favorites list (presumbly just before running it)"""
    foundMatch = False
    # check to see if this fav is already in the list
    for fav in game.system.FavsList:
        if CoerceGameName(fav.gameName, CoerceSystemName(fav.systemName)) == game and fav.rom == rom:
            foundMatch = True
            # if so, just update its "last time played"
            fav.lastPlayed = datetime.datetime.now()
    # otherwise, make a new entry in the list
    if foundMatch is False:
        lastPlayed = datetime.datetime.now()
        nuFav = Favorite(game.name, rom, lastPlayed, game.system.name)
        game.system.FavsList.append(nuFav)
    game.system.FavsList.sort() # sort the favs list, by time last played
    game.system.FavsList.reverse() # reverse so that latest is at top
    # keep the list from growing past its allotted length
    if len(game.system.FavsList) > favsListLength:
        game.system.FavsList.remove(game.system.FavsList[favsListLength])

def BuildGameList(system):
    "returns a GameList of the games in ROMpath for the specified system"
    if log:
        print 'Building game list for "%s"...' % system.name
    GameList = []
    FileList = []
    if log:
        print 'getting file list from %s' % system.ROMPath
    WalkDir(system.ROMPath, FileList)
    if len(FileList) == 0:
        return None
    FileList.sort()
    # use the MAME text list to match games with filenames
    if system.useMAMEList is True:
        MameList = []
        sysFile = 'data/%s' % system.name
        # if list has already been compiled, use that
        if os.path.exists(sysFile) is True:
            mameListFile = file(sysFile, 'rb')
            MameList = cPickle.Unpickler(mameListFile).load()
            mameListFile.close()
        # otherwise build it anew and store it
        else:
            print 'Building game list for "%s" from MAME database, please wait...' % system.name
            mame_list_start = datetime.datetime.now()
            mameSrc = file('data/mamelist.src').readlines()
            mameSrc.sort()
            for f in FileList:
                (name, i) = FindMameName(f, mameSrc)
                if name is not None:
                    if mame_list_speed:
                        mameSrc.pop(i)
                    MameList.append([name, f])
            if log:
                elapsed = datetime.datetime.now() - mame_list_start
                print 'MAME list took %s seconds to generate.' % elapsed
            # we now have a 2D array, with each row a file - gamename pair
            # store it!
            mameListFile = file(sysFile, 'wb')
            cPickle.Pickler(mameListFile, cPickle.HIGHEST_PROTOCOL).dump(MameList)
            mameListFile.close()
        for item in MameList:
            nugame = Game(item[0], item[1], system)
            GameList.append(nugame)
    # otherwise just see what files are in the ROMpath
    else:
        for f in FileList:
            name = os.path.basename(f)
            name = StripExtension(name)
            nugame = Game(name, f, system)
            GameList.append(nugame)
    GameList.sort()
    return GameList

def Extension(filename):
    "returns just the extension of the specified filename"
    return filename[filename.rfind('.')+1:len(filename)].lower()

def ROMList(game):
    "returns a list of ROMs in the specified game archive file"
    roms = []
    if Extension(game.path) == '7z':
        arcFile = py7zlib.Archive7z(file(game.path, 'rb'))
        for f in arcFile.files:
            roms.append(f.filename)
    elif Extension(game.path) == 'zip':
        pass # TODO: list contents of ZIP archive?
    elif Extension(game.path) == 'rar':
        pass # TODO: list contents of RAR archive?
    return roms

def ExtractROM(rom, game):
    "extracts the specified rom from the specified game archive file"
    if Extension(game.path) == '7z':
        arcFile = py7zlib.Archive7z(file(game.path, 'rb'))
        for f in arcFile.files:
            if f.filename == rom:
                romFile = open(rom, 'wb', 0)
                romFile.write(f.read())
                romFile.close()
    elif Extension(game.path) == 'zip':
        pass # TODO: extract rom from ZIP archive
    elif Extension(game.path) == 'rar':
        pass # TODO: extract rom from RAR archive

def AllSystemsGameList():
    "returns an amalgamated list of all games for all systems"
    MegaList = []
    for system in SystemList:
        if system not in [allSysName, exitName, shutdownName]:
            for game in system.GameList:
                MegaList.append(game)
    MegaList.sort()
    return MegaList

def AllSystemsFavList():
    "returns an amalgamated list of the top favs for all systems"
    MegaList = []
    for system in SystemList:
        if system not in [allSysName, exitName, shutdownName]:
            for fav in system.FavsList:
                MegaList.append(fav)
    MegaList.sort()
    MegaList.reverse()
    return MegaList[0:favsListLength]

# data structures

class Game:
    "an entry in a system's games list"
    def __init__(self, name, path, system):
        self.name = name
        self.path = path
        self.system = system
    def __cmp__(self, other):
        if self.name is not None and other.name is not None:
            return cmp(self.name, other.name)
        else:
            return None
    def __str__(self):
        return str(self.name)

class Favorite:
    "an entry in a system's favorites list"
    def __init__(self, gameName, rom, lastPlayed, systemName):
        # system and game are stored only as strings
        # to prevent having to store their massive
        # structures when we pickle it!
        self.gameName = gameName
        self.rom = rom
        self.lastPlayed = lastPlayed
        self.systemName = systemName
    def __cmp__(self, other):
        return cmp(self.lastPlayed, other.lastPlayed)
    def __str__(self):
        if CoerceSystemName(self.systemName).oneGameArchive is True:
            return str(self.gameName)
        else:
            return str(self.rom)

class System:
    "data structure for a game system, as defined in xml"
    def __init__(self, name, ROMPath, emuPath, emuArgs, useMAMEList, oneGameArchive, noJoystickEscape,
    noQuotes, systemPortrait, nameArt, systemDescription):
        self.name = name
        # determine ROM path exists
        if os.path.exists(ROMPath) is True:
            self.ROMPath = ROMPath
        else:
            print 'ROM path "%s" not found!  Check config.xml.' % ROMPath
            self.ROMPath = None
        # determine emulator path exists
        if os.path.exists(emuPath) is True:
            self.emuPath = emuPath
        else:
            print 'Emulator "%s" not found!  Check config.xml.' % emuPath
            self.emuPath = None
        if emuArgs is None or emuArgs is '':
            self.emuArgs = ''
        else:
            self.emuArgs = emuArgs
        self.useMAMEList = StringToBool(useMAMEList)
        self.oneGameArchive = StringToBool(oneGameArchive)
        self.noJoystickEscape = StringToBool(noJoystickEscape)
        self.noQuotes = StringToBool(noQuotes)
        if systemPortrait is not None and os.path.exists(systemPortrait) is True:
            self.systemPortrait = systemPortrait
        else:
            self.systemPortrait = None
            print 'System portrait art for "%s" not found!  Check config.xml.' % self.name
        if systemDescription is None or systemDescription is '':
            self.systemDescription = noDescText
        else:
            self.systemDescription = systemDescription
        if nameArt is not None and os.path.exists(nameArt) is True:
            self.nameArt = nameArt
        else:
            self.nameArt = None
        if self.ROMPath is not None:
            self.GameList = BuildGameList(self)
        self.FavsList = LoadFavsList(self)
    def __cmp__(self, other):
        return cmp(str(self), str(other))
    def __str__(self):
        return str(self.name)

# configuration functions

class ConfigHandler(xml.sax.handler.ContentHandler):
    "populates global variables with data from config.xml"
    def startElement(self, name, attrs):
        global fullscreen, playSounds, SystemList
        if name == 'System':
            if log:
                print 'Parsing XML for system %s...' % attrs.get('name')
            nuSys = System(attrs.get('name'), attrs.get('ROMPath'),
            attrs.get('emuPath'), attrs.get('emuArgs'),
            attrs.get('useMAMEList'), attrs.get('oneGameArchive'),
            attrs.get('noJoystickEscape'),
            attrs.get('noQuotes'), attrs.get('systemPortrait'),
            attrs.get('nameArt'), attrs.get('systemDescription'))
            # if a critical part of the System is missing, don't add it
            # to the system list!
            if nuSys.ROMPath is not None and nuSys.emuPath is not None:
                SystemList.append(nuSys)
        elif name == 'Joystick':
            pass
        elif name == 'Keyboard':
            pass
        elif name == 'Display':
            fullscreen = StringToBool(attrs.get('fullscreen'))
        elif name == 'Sound':
            playSounds = StringToBool(attrs.get('playSounds'))

def GetXMLConfig():
    "sets the XML parsing in motion"
    # Create a parser
    parser = xml.sax.make_parser()
    # Tell the parser we are not interested in XML namespaces
    parser.setFeature(xml.sax.handler.feature_namespaces, 0)
    # Create the handler
    dh = ConfigHandler()
    # Tell the parser to use our handler
    parser.setContentHandler(dh)
    # Parse the input
    # if we don't have a config.xml, make one from the default
    if not os.path.exists('config.xml'):
        print "Couldn't find config.xml, generating one from the default..."
        default_config = open('config_default.xml')
        new_config = open('config.xml', 'w')
        new_config.write(default_config.read())
        default_config.close()
        new_config.close()
    parser.parse('config.xml')

def BuildSystemList():
    global SystemList
    print 'Building game lists...'
    GetXMLConfig() # fills system list from XML
    SystemList.sort()
    nuList = [allSysName]
    for item in SystemList:
        nuList.append(item)
    SystemList = nuList
    SystemList.append(exitName)
    SystemList.append(shutdownName)
    print 'Done.'

# page drawing functions

def DrawSysListItem(itemText, itemNum, isSelectedItem):
    "draws a single system list item"
    barHeight = sysListHeight / sysScreenCharLength
    if isSelectedItem:
        color = baseColor
        # draw selection bar
        barSurf = pygame.Surface((sysListWidth, barHeight))
        barSurf.fill(selectBarColor)
        barX = sysListOffsetX
        barY = sysListOffsetY + barHeight * itemNum
        screen.blit(barSurf, (barX, barY))
    else:
        color = textColor
    # draw the text
    itemSurf = listFont.render(itemText, True, color)
    # if text is too big to fit on screen, use the small font
    if itemSurf.get_width() > sysListWidth:
        itemSurf = tinyFont.render(itemText, True, color)
    itemX = sysListOffsetX + 2
    itemY = sysListOffsetY + barHeight * itemNum
    itemY += (barHeight - itemSurf.get_height()) / 2 # re-center the item vertically
    screen.blit(itemSurf, (itemX, itemY))

def DrawSystemList():
    "sets up each system list item to be drawn by DrawSysListItem"
    i = 0
    for item in ScreenList:
        if i == pageSelIndex:
            DrawSysListItem(str(item), i, True)
        else:
            DrawSysListItem(str(item), i, False)
        i += 1

def DrawSystemDescription(system):
    "draws the system description text in the right panel"
    # decide which text to use
    if system == allSysName:
        text = allSysDescription
    elif system == exitName:
        text = exitDescription
    elif system == shutdownName:
        text = shutdownDescription
    else:
        text = system.systemDescription
    # split string into a list of words
    descList = text.split()
    descLines = []
    # whittle down the list of words until every one has been placed on a
    # properly sized line
    while len(descList) > 0:
        line = ''
        lineFull = False
        # keep adding words to the line while there still might be room
        while lineFull is False and len(descList) > 0:
            line += '%s ' % descList[0]
            # stop adding if we've run over the end
            if descFont.render(line, True, textColor).get_width() > sysDescWidth:
                # snip after second to last space
                line = line[0:line.rfind(' ', 0, -1)]
                lineFull = True
            # otherwise snip off the source list and add another next iteration
            else:
                del descList[0:1] # snip off first item
        # add the completed line to list
        descLines.append(line)
    # render each line in the list of lines
    i = 0
    for line in descLines:
        lineSurf = descFont.render(line, True, textColor)
        lineY = sysDescOffsetY + (descFontSize * i)
        # don't render lines that go off the bottom edge
        if lineY < sysDescOffsetY + sysDescHeight:
            screen.blit(lineSurf, (sysDescOffsetX, lineY))
        i += 1

def DrawSystemIcon(system):
    "draws the system portrait art in the lower right window"
    if system == allSysName:
        portrait = sysAllIcon
    elif system == exitName:
        portrait = sysExitIcon
    elif system == shutdownName:
        portrait = sysShutdownIcon
    elif system.systemPortrait is None:
        return None
    else:
        portrait = system.systemPortrait
    portSurf = pygame.image.load(portrait)
    screen.blit(portSurf, (sysIconOffsetX, sysIconOffsetY))

def DrawSystemPage():
    screen.blit(sysBGImage, sysBGImage.get_rect())
    DrawSystemList()
    DrawSystemDescription(ScreenList[pageSelIndex])
    DrawSystemIcon(ScreenList[pageSelIndex])
    pygame.display.flip()

def DrawGameListItem(itemText, itemNum, isSelectedItem):
    "draws a single game list item"
    barHeight = gameListHeight / gameScreenCharLength
    if isSelectedItem:
        color = baseColor
        # draw selection bar
        barSurf = pygame.Surface((gameListWidth, barHeight))
        barSurf.fill(selectBarColor)
        barX = gameListOffsetX
        barY = gameListOffsetY + barHeight * itemNum
        screen.blit(barSurf, (barX, barY))
    else:
        color = textColor
    # draw the text
    itemSurf = gameListFont.render(itemText, True, color)
    # if text is too big to fit on screen, use the small font
    if itemSurf.get_width() > gameListWidth:
        itemSurf = tinyFont.render(itemText, True, color)
    itemX = gameListOffsetX + 2
    itemY = gameListOffsetY + barHeight * itemNum
    itemY += (barHeight - itemSurf.get_height()) / 2 # re-center the item vertically
    screen.blit(itemSurf, (itemX, itemY))

def DrawFavList():
    i = 0
    for item in ScreenList:
        if i == pageSelIndex:
            DrawGameListItem(str(item), i, True)
        else:
            DrawGameListItem(str(item), i, False)
        i += 1

def DrawGameList():
    "sets up each game list item to be drawn by DrawGameListItem"
    i = 0
    for item in ScreenList:
        if i == pageSelIndex:
            DrawGameListItem(str(item), i, True)
        else:
            DrawGameListItem(str(item), i, False)
        i += 1

def DrawGameListNum():
    "draws the 'X out of Y total' readout"
    num = pageScreenIndex + pageSelIndex + 1
    if len(PageList) > 0:
        numstring = '%s/%s' % (num, len(PageList))
    else:
        numstring = '0/0'
    numSurf = gameNumFont.render(numstring, True, textColor)
    numX = screenWidth - gameNumOffsetXneg - numSurf.get_width()
    screen.blit(numSurf, (numX, gameNumOffsetY))

def DrawSystemName(nameArt):
    "draws the badge with the system's name on the game list page"
    if nameArt is not None:
        nameSurf = pygame.image.load(nameArt)
        nameY = screenHeight - nameSurf.get_height()
        screen.blit(nameSurf, (0, nameY))

def DrawFullFavToggle(drawFull):
    """draws the game list page element that indicates whether
    the Full or Favorites list is selected"""
    barSurf = pygame.Surface((toggleWidth, toggleHeight))
    # highlight "Full"
    if drawFull is True:
        # position selection bar
        barX = fullToggleOffsetX
        barY = fullToggleOffsetY
        # draw "Full" text
        fullSurf = subTitleFont.render(fullText, True, baseColor)
        # draw "Fav" text
        favSurf = subTitleFont.render(favText, True, textColor)
    # highlight "Favorites"
    else:
        # position selection bar
        barX = favToggleOffsetX
        barY = favToggleOffsetY
        # draw "Full" text
        fullSurf = subTitleFont.render(fullText, True, textColor)
        # draw "Fav" text
        favSurf = subTitleFont.render(favText, True, baseColor)
    # position text
    fullX = fullToggleOffsetX
    fullX += (toggleWidth - fullSurf.get_width()) / 2
    fullY = fullToggleOffsetY
    fullY += (toggleHeight - fullSurf.get_height()) / 2
    favX = favToggleOffsetX
    favX += (toggleWidth - favSurf.get_width()) / 2
    favY = favToggleOffsetY
    favY += (toggleHeight - favSurf.get_height()) / 2
    # draw the elements
    barSurf.fill(selectBarColor)
    screen.blit(barSurf, (barX, barY))
    screen.blit(fullSurf, (fullX, fullY))
    screen.blit(favSurf, (favX, favY))

def DrawGameFullPage():
    screen.blit(gameBGImage, gameBGImage.get_rect())
    DrawFullFavToggle(True)
    DrawGameList()
    DrawGameListNum()
    if len(ScreenList) > 0:
        if SelectedSystem == allSysName:
            # use the name badge for the currently selected game's system!
            DrawSystemName(ScreenList[pageSelIndex].system.nameArt)
        else:
            DrawSystemName(SelectedSystem.nameArt)
    pygame.display.flip()

def DrawGameFavPage():
    screen.blit(gameBGImage, gameBGImage.get_rect())
    DrawFullFavToggle(False)
    DrawFavList()
    DrawGameListNum()
    if len(ScreenList) > 0:
        if SelectedSystem == allSysName:
            # use the name badge for the currently selected game's system!
            sys = CoerceSystemName(ScreenList[pageSelIndex].systemName)
            DrawSystemName(sys.nameArt)
        else:
            DrawSystemName(SelectedSystem.nameArt)
    pygame.display.flip()

def DrawROMList():
    "sets up each rom list item to be drawn by DrawGameListItem"
    i = 0
    for item in ScreenList:
        if i == pageSelIndex:
            DrawGameListItem(str(item), i, True)
        else:
            DrawGameListItem(str(item), i, False)
        i += 1

def DrawGameName(name):
    "draws the selected game's name at the bottom of the rom list page"
    nameSurf = titleFont.render(name, True, textColor)
    # use biggest font, but keep trying smaller fonts until it fits
    if nameSurf.get_width() > romGameNameWidth:
        nameSurf = listFont.render(name, True, textColor)
        if nameSurf.get_width() > romGameNameWidth:
            nameSurf = gameListFont.render(name, True, textColor)
            if nameSurf.get_width() > romGameNameWidth:
                nameSurf = tinyFont.render(name, True, textColor)
    # center name in bottom panel
    titleX = romGameNameOffsetX
    titleX += (romGameNameWidth - nameSurf.get_width()) / 2
    titleY = romGameNameOffsetY
    titleY += (romGameNameHeight - nameSurf.get_height()) / 2
    screen.blit(nameSurf, (titleX, titleY))

def DrawROMPage():
    screen.blit(romBGImage, romBGImage.get_rect())
    DrawROMList()
    DrawGameName(SelectedGame.name)
    pygame.display.flip()

def DrawCurrentPage():
    if currentPage == 'System':
        DrawSystemPage()
    elif currentPage == 'GameFull':
        DrawGameFullPage()
    elif currentPage == 'GameFav':
        DrawGameFavPage()
    elif currentPage == 'ROM':
        DrawROMPage()

def DrawSleepScreen():
    screen.blit(sleepImage, sleepImage.get_rect())
    pygame.display.flip()

# emulator-related functions

def RunROM(rom):
    global runningEmu, runningROM, emuProcess, emuPID
    global screen
    runningEmu = True # prevent keyboard inputs
    # save name of ROM running in case we want to delete it afterwards
    runningROM = rom
    # turn unicode from XML into strings
    runcmd = str(SelectedGame.system.emuPath)
    # enclose it in quotes for paths with spaces
    runcmd = '"%s"' % runcmd
    emuDir = os.getcwd()
    # add arguments if they have been specified in XML
    if SelectedGame.system.emuArgs != '':
        runcmd += ' %s' % SelectedGame.system.emuArgs
    # some emulators add their own quotes
    if SelectedGame.system.noQuotes is False:
        rom = '"%s"' % rom
    runcmd += ' %s' % rom
    # if we're in fullscreen, go into window before launching emu
    # to prevent focus problems
    if fullscreen is True:
        screen = pygame.display.set_mode(screenSize)
    if 'win' in sys.platform:
        # spawn process in proper, torturous win32 API way
        (emuProcess, thread, emuPID, threadID) = win32process.CreateProcess(SelectedGame.system.emuPath,
        runcmd,
        None,
        None,
        1,
        0,
        None,
        emuDir,
        win32process.STARTUPINFO())
    elif 'linux' in sys.platform:
        emuProcess = popen2.Popen3(runcmd, True)

def ShutdownWindows():
    #os.execl('shutdown.exe', ' -u -t 1')
    win32shutdown.shutdown(gethostname())

def ShutdownLinux():
    os.system('shutdown')
    #Quit()

# user input functions

def SelectROM(rom):
    AddToFavsList(SelectedGame, rom)
    StoreFavsList(SelectedGame.system)
    # draw "please wait" screen
    DrawSleepScreen()
    SwitchWinDir()
    ExtractROM(rom, SelectedGame)
    RunROM(rom)

def SelectFav(fav):
    global PageList, ScreenList
    global pageSelIndex, pageScreenIndex
    global SelectedGame
    SelectedGame = CoerceGameName(fav.gameName, fav.systemName)
    # update the favs list
    AddToFavsList(SelectedGame, fav.rom)
    StoreFavsList(CoerceSystemName(fav.systemName))
    # recalculate Page and Screen lists and redraw
    if SelectedSystem == allSysName:
        PageList = AllSystemsFavList()
    else:
        PageList = SelectedSystem.FavsList
    # reset place in list (latest pick will be at top anyway)
    pageSelIndex = 0
    pageScreenIndex = 0
    ScreenList = PageList[0:min(screenListLength, len(PageList))]
    DrawCurrentPage()
    # draw "please wait" screen
    DrawSleepScreen()
    # extract if necessary then run
    SwitchWinDir()
    if CoerceSystemName(fav.systemName).oneGameArchive is not True:
        ExtractROM(fav.rom, SelectedGame)
    RunROM(fav.rom)

def SelectGame(game):
    """marks the game as 'selected' and goes to its ROM list,
    or plays it if it's a MAME game"""
    global currentPage, PageList, ScreenList, screenListLength, SelectedGame
    global fullSelIndex, fullScreenIndex
    global pageSelIndex, pageScreenIndex
    # save place in full game list
    fullSelIndex = pageSelIndex
    fullScreenIndex = pageScreenIndex
    SelectedGame = game
    if game.system.oneGameArchive is True:
        AddToFavsList(SelectedGame, SelectedGame.path)
        StoreFavsList(SelectedGame.system)
        # draw "please wait" screen
        DrawSleepScreen()
        SwitchWinDir()
        RunROM(game.path)
    else:
        PageList = ROMList(game)
        screenListLength = gameScreenCharLength
        ScreenList = PageList[0:min(screenListLength, len(PageList))]
        currentPage = 'ROM'
        # reset place in page list
        pageSelIndex = 0
        pageScreenIndex = 0
        DrawCurrentPage()

def SelectSystem(system):
    "marks the system as 'selected' and goes to its game list"
    global currentPage, PageList, ScreenList, screenListLength, SelectedSystem
    global sysSelIndex, sysScreenIndex
    global pageSelIndex, pageScreenIndex
    # save place in system list
    sysSelIndex = pageSelIndex
    sysScreenIndex = pageScreenIndex
    if system == allSysName:
        SelectedSystem = allSysName
        PageList = AllSystemsGameList()
        screenListLength = gameScreenCharLength
        ScreenList = PageList[0:min(screenListLength, len(PageList))]
    elif system == exitName:
        Quit()
    elif system == shutdownName:
        if 'win' in sys.platform:
            ShutdownWindows()
        elif 'linux' in sys.platform:
            ShutdownLinux()
    else:
        SelectedSystem = system
        PageList = system.GameList
        screenListLength = gameScreenCharLength
        ScreenList = PageList[0:min(screenListLength, len(PageList))]
    # reset place in page list
    pageSelIndex = 0
    pageScreenIndex = 0
    currentPage = 'GameFull'
    DrawCurrentPage()

def BailGameSelect():
    "backs out to the system list from the game list"
    global SelectedSystem, currentPage, PageList, screenListLength, ScreenList
    global fullSelIndex, fullScreenIndex, favSelIndex, favScreenIndex
    global pageSelIndex, pageScreenIndex
    SelectedSystem = None
    PageList = SystemList
    screenListLength = sysScreenCharLength
    # reset places in game lists
    fullSelIndex = 0 # full game listsysSelIndex = 0 # selected item in system list
    fullScreenIndex = 0
    favSelIndex = 0 # "recent" game list
    favScreenIndex = 0
    # restore place in system list
    pageSelIndex = sysSelIndex
    pageScreenIndex = sysScreenIndex
    ScreenList = PageList[pageScreenIndex:pageScreenIndex+screenListLength]
    currentPage = 'System'
    DrawCurrentPage()

def BailROMSelect():
    "backs out to the game list from the ROM list"
    global currentPage, PageList, ScreenList, screenListLength
    global SelectedGame
    global pageSelIndex, pageScreenIndex
    SelectedGame = None
    if SelectedSystem == allSysName:
        PageList = AllSystemsGameList()
    else:
        PageList = SelectedSystem.GameList
    screenListLength = gameScreenCharLength
    # restore place in game list
    pageSelIndex = fullSelIndex
    pageScreenIndex = fullScreenIndex
    # recalculate what's on the screen
    ScreenList = PageList[pageScreenIndex:pageScreenIndex+screenListLength]
    currentPage = 'GameFull'
    DrawCurrentPage()

def YesPressed():
    if len(ScreenList) > 0 and ScreenList[pageSelIndex] is not None:
        if playSounds is True:
            yesSound.play()
        if currentPage == 'System':
            SelectSystem(ScreenList[pageSelIndex])
        elif currentPage == 'GameFull':
            SelectGame(ScreenList[pageSelIndex])
        elif currentPage == 'GameFav':
            SelectFav(ScreenList[pageSelIndex])
        elif currentPage == 'ROM':
            SelectROM(ScreenList[pageSelIndex])

def NoPressed():
    if playSounds is True:
        noSound.play()
    if currentPage == 'GameFull' or currentPage == 'GameFav':
        BailGameSelect()
    elif currentPage == 'ROM':
        BailROMSelect()

def RandomPressed():
    "navigates to a randomly chosen item on the current page list"
    global pageSelIndex, pageScreenIndex, ScreenList
    pick = randint(0, len(PageList)-1)
    # list is only one screen long
    if len(PageList) <= screenListLength:
        pageSelIndex = pick
    else:
        # adjust selection position if we can't move the screen to
        # the user's existing selection
        pageScreenIndex = pick - pageSelIndex
        # screen is too close to bottom to keep selection bar where it is
        if pageScreenIndex + screenListLength > len(PageList):
            # go to last page and reposition selection
            pageScreenIndex = len(PageList) - screenListLength
            pageSelIndex = pick - pageScreenIndex
        # screen is too close to top
        elif pageScreenIndex < 0:
            # go to first page and reposition selection
            pageScreenIndex = 0
            pageSelIndex = pick
        # recalculate what's on screen
        ScreenList = PageList[pageScreenIndex:pageScreenIndex+screenListLength]
    if playSounds is True:
        randomSound.play()
    DrawCurrentPage()

def Quit():
    print 'Thanks for using %s' % (appName)
    sys.exit()

# list navigation

def UpPressed():
    "decrements the screen list by a single item"
    global pageSelIndex, pageScreenIndex, ScreenList
    listLength = min(screenListLength, len(PageList))
    # at top of screen
    if pageSelIndex == 0:
        # at top of list too
        if pageScreenIndex == 0:
            # wrap to bottom
            pageSelIndex = listLength - 1
            pageScreenIndex = max(0, len(PageList) - screenListLength)
            ScreenList = PageList[pageScreenIndex:pageScreenIndex+listLength]
        else:
            # scroll up one
            pageScreenIndex -= 1
            ScreenList = PageList[pageScreenIndex:pageScreenIndex+listLength]
    else:
        # move selection up one
        pageSelIndex -= 1
    if playSounds is True:
        selSound.play()
    DrawCurrentPage()

def DownPressed():
    "increments the screen list by a single item"
    global pageSelIndex, pageScreenIndex, ScreenList
    listLength = min(screenListLength, len(PageList))
    # at bottom of screen
    if pageSelIndex >= listLength - 1:
        # at bottom of list too
        if pageScreenIndex + listLength >= len(PageList):
            # wrap to top
            pageSelIndex = 0
            pageScreenIndex = 0
            ScreenList = PageList[0:listLength]
        else:
            # scroll down one
            pageScreenIndex += 1
            ScreenList = PageList[pageScreenIndex:pageScreenIndex+listLength]
    else:
        # move selection down one
        pageSelIndex += 1
    if playSounds is True:
        selSound.play()
    DrawCurrentPage()

def PgUpPressed():
    "decrements the screen list by a full page"
    global pageSelIndex, pageScreenIndex, ScreenList
    # list is only one screen long
    if len(PageList) <= screenListLength:
        # jump to first item
        pageSelIndex = 0
    else:
        # on first page of list
        if pageScreenIndex == 0:
            # wrap to last page at bottom of list
            pageScreenIndex = len(PageList) - screenListLength
        # less than a full page from first page
        elif pageScreenIndex <= screenListLength:
            # go to first page
            pageScreenIndex = 0
        # more than one page from top
        else:
            pageScreenIndex -= screenListLength
        # recalculate what's on the screen
        ScreenList = PageList[pageScreenIndex:pageScreenIndex+screenListLength]
    if playSounds is True:
        pageSound.play()
    DrawCurrentPage()

def PgDnPressed():
    "increments the screen list by a full page"
    global pageSelIndex, pageScreenIndex, ScreenList
    # list is only one screen long
    if len(PageList) <= screenListLength:
        # jump to first item
        pageSelIndex = len(PageList) - 1
    else:
        # on last page of list
        if pageScreenIndex >= len(PageList) - screenListLength:
            # wrap to first page at top of list
            pageScreenIndex = 0
        # less than a full page from last page
        elif pageScreenIndex >= len(PageList)-(screenListLength*2)+1:
            # go to last page
            pageScreenIndex = len(PageList)-screenListLength
        # more than one page from bottom
        else:
            pageScreenIndex += screenListLength
        # recalculate what's on the screen
        ScreenList = PageList[pageScreenIndex:pageScreenIndex+screenListLength]
    if playSounds is True:
        pageSound.play()
    DrawCurrentPage()

def FullListPressed():
    "switches to full list if in favs list"
    global currentPage, PageList, ScreenList, pageSelIndex, pageScreenIndex
    if currentPage == 'GameFav':
        pageSelIndex = fullSelIndex
        pageScreenIndex = fullScreenIndex
        if SelectedSystem == allSysName:
            PageList = AllSystemsGameList()
        else:
            PageList = SelectedSystem.GameList
        ScreenList = PageList[pageScreenIndex:pageScreenIndex+screenListLength]
        currentPage = 'GameFull'
        DrawCurrentPage()

def FavsListPressed():
    "switches to favs list if in full list"
    global currentPage, PageList, ScreenList, pageSelIndex, pageScreenIndex
    global fullSelIndex, fullScreenIndex
    if currentPage == 'GameFull':
        fullSelIndex = pageSelIndex
        fullScreenIndex = pageScreenIndex
        if SelectedSystem == allSysName:
            PageList = AllSystemsFavList()
        else:
            PageList = SelectedSystem.FavsList
        ScreenList = PageList[0:min(screenListLength, len(PageList))]
        pageSelIndex = 0
        pageScreenIndex = 0
        currentPage = 'GameFav'
        DrawCurrentPage()

def emuEWcallback(hwnd, dummy):
    "EnumWindows callback - sends WM_CLOSE to emu process's main window"
    TId, PId = win32process.GetWindowThreadProcessId(hwnd)
    if PId == emuPID:
        win32gui.PostMessage(hwnd, win32con.WM_QUIT, 0, 0)

def EmuWait(mSec=None):
    "Wait for process to finish or for specified number of milliseconds to elapse."
    if mSec is None:
        mSec = win32event.INFINITE
    return win32event.WaitForSingleObject(emuProcess, mSec)

def WinKillEmu():
    win32gui.EnumWindows(emuEWcallback, 0)
    if EmuWait(int(emuCloseTimeout * 1000)) != win32event.WAIT_OBJECT_0:
        # app can't close, terminate it
        win32process.TerminateProcess(emuProcess, 0)
        win32api.Sleep(100) # wait for resources to be released

def LinuxKillEmu():
    os.kill(emuProcess.pid, 3)

def StopEmu():
    "handles emulator shutdown, cleanup and returns EMUI to normal operation"
    global runningEmu, runningROM, screen
    # wait a bit to allow the process to exit normally
    sleep(1)
    # once emu has exited, delete extracted ROM
    if SelectedGame.system.oneGameArchive is False:
        os.remove(runningROM)
    runningROM = None
    # see if we can successfully resume fullscreen mode
    if fullscreen is True:
        try:
            screen = pygame.display.set_mode(screenSize, pygame.FULLSCREEN)
        except:
            print "Couldn't restore fullscreen - SDL exclusive mode already set?"
    if 'win' in sys.platform:
        os.chdir(emuiDir)
    # clear all events from queue - necessary to "forget" keystrokes
    # user has made while playing emulator!!
    pygame.event.clear()
    runningEmu = False # re-allow input
    DrawCurrentPage()

def MainLoop():
    # emulator is running, listen for process end or escape sequence
    global idle_time
    if runningEmu is True:
        sleep(1) # don't poll constantly
        # check for escape sequence
        if SelectedGame.system.noJoystickEscape is False and EscSequenceEntered() is True:
            if 'win' in sys.platform:
                WinKillEmu()
            elif 'linux' in sys.platform:
                LinuxKillEmu()
            StopEmu()
        # process has exited 'normally', successfully or no
        elif ('win' in sys.platform and win32process.GetExitCodeProcess(emuProcess) != win32con.STILL_ACTIVE) or ('linux' in sys.platform and emuProcess.poll() != -1):
            StopEmu()
    else:
        if idle_time >= (attract_idle_time * inputRepeatRate):
            print 'idle_time is %s, entering idle mode!' % idle_time
        for event in pygame.event.get():
            idle = True
            # process keyboard input events
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    Quit()
                elif event.key in yesKeys:
                    YesPressed()
                elif event.key in noKeys:
                    NoPressed()
                elif event.key in randomKeys:
                    RandomPressed()
                elif event.key in upKeys:
                    UpPressed()
                elif event.key in downKeys:
                    DownPressed()
                elif event.key in pgUpKeys:
                    PgUpPressed()
                elif event.key in pgDnKeys:
                    PgDnPressed()
                elif event.key in fullListKeys:
                    FullListPressed()
                elif event.key in favsListKeys:
                    FavsListPressed()
                idle = False
            # process joystick input events
            if event.type == pygame.JOYBUTTONDOWN:
                if event.button in yesButtons:
                    YesPressed()
                elif event.button in noButtons:
                    NoPressed()
                elif event.button in randomButtons:
                    RandomPressed()
                elif event.button in pgUpButtons:
                    PgUpPressed()
                elif event.button in pgDnButtons:
                    PgDnPressed()
                elif event.button in toFullButtons:
                    FullListPressed()
                elif event.button in toFavButtons:
                    FavsListPressed()
                idle = False
            # process joystick axis numbers every tick
            if pad is not None and event.type == pygame.USEREVENT:
                if pad.get_axis(selAxis) < -stickDeadZone:
                    idle = False
                    UpPressed()
                elif pad.get_axis(selAxis) > stickDeadZone:
                    idle = False
                    DownPressed()
            # if no events happened, we are idle
            if idle:
                #print 'idle'
                idle_time += 1
            else:
                pass
                #print 'not idle'

if __name__ == "__main__":
    # create dir for favs files if it doesn't exist
    if os.path.exists('fav') is False:
        os.mkdir('fav')
    BuildSystemList() # sets up the master data structure
    pygame.init()
    # load in external art content
    titleFont = pygame.font.Font(titleFontFile, titleFontSize)
    subTitleFont = pygame.font.Font(titleFontFile, subTitleFontSize)
    listFont = pygame.font.Font(listFontFile, listFontSize)
    descFont = pygame.font.Font(listFontFile, descFontSize)
    gameListFont = pygame.font.Font(listFontFile, gameListFontSize)
    gameNumFont = pygame.font.Font(listFontFile, gameNumFontSize)
    tinyFont = pygame.font.Font(tinyFontFile, tinyFontSize)
    sysBGImage = pygame.image.load(sysBGImageFile)
    gameBGImage = pygame.image.load(gameBGImageFile)
    romBGImage = pygame.image.load(romBGImageFile)
    sleepImage = pygame.image.load(sleepImageFile)
    iconImage = pygame.image.load(appIcon)
    # set up sounds
    if playSounds is True:
        pygame.mixer.init()
        yesSound = pygame.mixer.Sound(yesSoundFile)
        noSound = pygame.mixer.Sound(noSoundFile)
        randomSound = pygame.mixer.Sound(randomSoundFile)
        selSound = pygame.mixer.Sound(selSoundFile)
        pageSound = pygame.mixer.Sound(pageSoundFile)
    # set up joystick
    pygame.joystick.init()
    if pygame.joystick.get_count() > 0:
        pad = pygame.joystick.Joystick(stickNumber)
        pad.init()
    # set keyboard auto-repeat
    pygame.key.set_repeat(inputDelay, inputRepeatRate)
    # create the window and draw the screen
    if fullscreen is True:
        screen = pygame.display.set_mode(screenSize, pygame.FULLSCREEN)
        pygame.mouse.set_visible(False)
    else:
        screen = pygame.display.set_mode(screenSize)
    pygame.display.set_caption(appName)
    pygame.display.set_icon(iconImage)
    # start on "System Select" page at top of system list
    PageList = SystemList
    screenListLength = sysScreenCharLength
    ScreenList = PageList[0:min(screenListLength, len(PageList))]
    currentPage = 'System'
    DrawCurrentPage()
    # set joystick repeat timer
    pygame.time.set_timer(pygame.USEREVENT, inputRepeatRate)
    while True:
        MainLoop()
